package tmacsoftware.ursql;

import java.io.File;
import java.util.Vector;

public class UrSQLController
{
    // path to database file
    private String dbPath;
    // database file object
    private File dbFile;
    // file i/o object
    private Filio filio;
    // constant to separate keys and values
    public static final String KEY_VALUE_SEPARATOR = "=";
    // constant to separate key/value pair queries
    public static final String QUERY_SEPARATOR = ";";
    // constant used in private methods to get a key from a key/value pair
    private static final int GET_KEY = 0;
    // constant used in private methods to get a value form a key/value pair
    private static final int GET_VALUE = 1;
    // string to write as first line of new database
    private static final String BLANK_DB_FIRST_LINE = "[0]";

    /**
     * Create a new controller
     * @param dbPath Path to database file
     */
    public UrSQLController(String dbPath)
    {
        // save properties
        this.dbPath = dbPath;
        this.dbFile = new File(dbPath);
        this.filio = new Filio(dbFile);

        // create database if blank
        if (filio.read().compareTo("") == 0)
        {
            this.createDatabase();
        }
    }

    /**
     * Get path to database file
     * @return Path to database file
     */
    public String getDbPath()
    {
        return this.dbPath;
    }

    /**
     * Set path to database file
     * @param dbPath Path to database file
     */
    public void setFilename(String dbPath)
    {
        this.dbPath = dbPath;
    }

    /**
     * Get database file object
     * @return Database file
     */
    public File getDbFile()
    {
        return this.dbFile;
    }

    /**
     * Set database file object
     * @param dbFile Database file
     */
    public void setDbFile(File dbFile)
    {
        this.dbFile = dbFile;
    }

    /**
     * Get file i/o object
     * @return Filio object for database
     */
    public Filio getFilio()
    {
        return this.filio;
    }

    /**
     * Set file i/o object
     * @param filio Filio object for database
     */
    public void setFilio(Filio filio)
    {
        this.filio = filio;
    }

    /**
     * Count the number of entities in the database
     * @return
     */
    public int countEntities()
    {
        Vector<UrSQLEntity> entities = this.getAllEntities();
        return entities.size();
    }

    /**
     * Get all ids in the database
     * @return Vector containing all integers
     */
    private Vector<Integer> getAllIds()
    {
        Vector<Integer> returnVector = new Vector<Integer>();
        // iterate through entire file
        for (int i = 0; i < filio.getTotalLines(); i++)
        {
            String line = filio.read(i);
            // add integer id to vector if line is an id
            if (isIdLine(line))
            {
                returnVector.add(idToInt(line));
            }
        }
        return returnVector;
    }

    /**
     * Check if a given line contains an id for an entity
     * @param line String to check is id
     * @return true if line is an id line, false if not
     */
    private boolean isIdLine(String line)
    {
        // id lines are formatted as [0]
        return line.contains("[") && line.contains("]");
    }

    /**
     * Remove first character of string if it is whitespace
     * @param string String to alter
     * @return String without whitespace as first character
     */
    private String removeWhitespace(String string)
    {
        if (string.compareTo("") == 0)
        {
            return "";
        }
        // get first and last characters as string
        String firstChar = Character.toString(string.charAt(0));
        String lastChar = Character.toString(string.charAt(string.length() - 1));
        // check if first character is whitespace
        if (firstChar.compareTo(" ") == 0)
        {
            // remove first character
            string = string.substring(1);
        }
        // check if last character is whitespace
        if (lastChar.compareTo(" ") == 0)
        {
            // remove last character
            string = string.substring(0, string.length() - 1);
        }

        return string;
    }

    /**
     * Get a single key or a value from a query
     * @param query Formatted key/value pair
     * @param type Get either key or value from formatted query
     * @return Key or value from query
     */
    private String getFromQuery(String query, int type)
    {
        try
        {
            // split keys and values
            String[] splitQuery = query.split(KEY_VALUE_SEPARATOR);

            // remove whitespace if it is the first character in key or value
            for (int i = 0; i < splitQuery.length; i++)
            {
                splitQuery[i] = removeWhitespace(splitQuery[i]);

            }
            // return either key or value according to type
            return (type == GET_KEY) ? splitQuery[0] : splitQuery[1];
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            return "";
        }
    }

    /**
     * Get a key from a query
     * @param query Formatted key/value pair
     * @return Key from query
     */
    private String getKeyFromQuery(String query)
    {
        return getFromQuery(query, GET_KEY);
    }

    /**
     * Get a value from a query
     * @param query Formatted key/value pair
     * @return Value from query
     */
    private String getValueFromQuery(String query)
    {
        return getFromQuery(query, GET_VALUE);
    }

    /**
     * Get all the keys and values from a query containing more than one parameter
     * @param query Formatted key/value pairs
     * @param type Get either keys or values
     * @return Keys or values from query
     */
    private Vector<String> getMultipleFromQuery(String query, int type)
    {
        Vector<String> returnVector = new Vector<String>();
        // avoid array out of bounds exception if query contains 1 parameter
        if (!query.contains(QUERY_SEPARATOR))
        {
            String returnString = getFromQuery(query, type);
            returnVector.add(returnString);
            return returnVector;
        }
        String[] queries = query.split(QUERY_SEPARATOR);
        // iterate through queries
        for (int i = 0; i < queries.length; i++)
        {
            // split key and value
            String[] singleQuery = queries[i].split(KEY_VALUE_SEPARATOR);
            // remove whitespace
            for (int j = 0; j < singleQuery.length; j++)
            {
                singleQuery[j] = removeWhitespace(singleQuery[j]);
            }
            // return according to type
            if (type == GET_KEY)
            {
                returnVector.add(singleQuery[0]);
            }
            else
            {
                if (type == GET_VALUE)
                {
                    returnVector.add(singleQuery[1]);
                }
            }
        }
        return returnVector;
    }

    /**
     * Get all the keys from a query containing more than one parameter
     * @param query Formatted key/value pairs
     * @return All keys from query
     */
    private Vector<String> getKeysFromQuery(String query)
    {
        return getMultipleFromQuery(query, GET_KEY);
    }

    /**
     * Get all the values from a query containing more than one parameter
     * @param query Formatted key/value pairs
     * @return All values from query
     */
    private Vector<String> getValuesFromQuery(String query)
    {
        return getMultipleFromQuery(query, GET_VALUE);
    }

    /**
     * Get all the keys or values in an entity with the given id
     * @param id ID of entity
     * @param type Get either keys or values
     * @return All keys or values of a given entity
     */
    private Vector<String> getFromId(int id, int type)
    {
        Vector<String> returnVector = new Vector<String>();
        boolean foundId = false;
        // string representation of desired id lines
        String compare = "[" + id + "]";
        // iterate through file
        for (int i = 0; i < filio.getTotalLines(); i++)
        {
            // read line from file
            String line = filio.read(i);
            // if  next ID line is found, restart search
            if (isIdLine(line))
            {
                foundId = false;
            }
            // id line found
            if (line.compareTo(compare) == 0)
            {
                foundId = true;
            }
            // id line found, so get appropriate keys or values
            if (foundId && line.compareTo(compare) != 0)
            {
                if (type == GET_KEY)
                {
                    returnVector.add(getKeyFromQuery(line));
                }
                else
                {
                    if (type == GET_VALUE)
                    {
                        returnVector.add(getValueFromQuery(line));
                    }
                }
            }
        }
        return returnVector;
    }

    /**
     * Get all the keys in an entity with the given id
     * @param id ID of entity
     * @return All keys contained in entity
     */
    private Vector<String> getKeysFromId(int id)
    {
        return getFromId(id, GET_KEY);
    }

    /**
     * Get all the values in an entity with the given id
     * @param id ID of entity
     * @return All values contained in entity
     */
    private Vector<String> getValuesFromId(int id)
    {
        return getFromId(id, GET_VALUE);
    }

    /**
     * Convert a formatted id line to an integer
     * @param id String ID representation
     * @return Integer of ID
     */
    private int idToInt(String id)
    {
        // strip everything but integer of id
        id = id.replace("[", "");
        id = id.replace("]", "");
        return Integer.parseInt(id);
    }

    /**
     * Get the id of the entity with the given parameters
     * @param query Formatted key/value pairs
     * @return Integer ID of entity
     */
    private int getIdFromQuery(String query)
    {
        // get keys and values from query
        Vector<String> queryKeys = getKeysFromQuery(query);
        Vector<String> queryValues = getValuesFromQuery(query);
        Vector<Integer> ids = getAllIds();
        int hits = 0;
        int returnId = 0;
        // iterate through entities
        for (int i = 0; i < ids.size(); i++)
        {
            Vector<String> matchKeys = getKeysFromId(ids.get(i));
            Vector<String> matchValues = getValuesFromId(ids.get(i));
            // iterate through query keys
            for (int j = 0; j < queryKeys.size(); j++)
            {
                // iterate through matching keys
                for (int k = 0; k < matchKeys.size(); k++)
                {
                    // if key and value matches, increment hit counter
                    if (queryKeys.get(j).compareTo(matchKeys.get(k)) == 0 &&
                            queryValues.get(j).compareTo(matchValues.get(k)) == 0)
                    {
                        hits++;
                        break;
                    }
                }
            }
            // if all keys/values match, then appropriate entity has been found
            if (hits == queryKeys.size())
            {
                return ids.get(i);
            }
            // restart hit counter
            else
            {
                hits = 0;
            }
        }
        return returnId;
    }

    /**
     * Get the entry with the given key in the entity of the given id
     * @param id ID of entity
     * @param key Key for entry
     * @return Entry in entity with given key
     */
    private UrSQLEntry getEntryFromId(String key, int id)
    {
        // get all keys and values from given id
        Vector<String> keys = getKeysFromId(id);
        Vector<String> values = getValuesFromId(id);
        // create new entry to return
        UrSQLEntry returnEntry = new UrSQLEntry();
        // iterate through keys
        for (int i = 0; i < keys.size(); i++)
        {
            // return if key matches desired key
            if (keys.get(i).compareTo(key) == 0)
            {
                returnEntry = new UrSQLEntry(keys.get(i), values.get(i));
            }
        }
        return returnEntry;
    }

    /**
     * Get the entry with the given key in an entity containing the given parameters
     * @param key Key of entry
     * @param query Parameters contained in entity
     * @return Entry in entity that matches parameters
     */
    public UrSQLEntry getEntry(String key, String query)
    {
        // get ID then use above method
        int id = getIdFromQuery(query);
        UrSQLEntry returnEntry = getEntryFromId(key, id);
        // get the entity the entry belongs to and save it
        UrSQLEntity entity = getEntity(id);
        returnEntry.setEntity(entity);
        return returnEntry;
    }

    /**
     * Get the entry with the given key from the given entity
     * @param key Key of entry 
     * @param entity Entity containing key 
     * @return Entry in entity
     */
    public UrSQLEntry getEntry(String key, UrSQLEntity entity)
    {
        // get ID from entity
        UrSQLEntry returnEntry = getEntryFromId(key, entity.getId());
        return returnEntry;
    }

    /**
     * Get an entity with the given id
     * @param id ID of entity
     * @return Entity with given ID
     */
    public UrSQLEntity getEntity(int id)
    {
        // create entity to return
        UrSQLEntity returnEntity = new UrSQLEntity(id);
        // get all keys and values for entity
        Vector<String> keys = getKeysFromId(id);
        Vector<String> values = getValuesFromId(id);
        // iterate through entries
        for (int i = 0; i < keys.size(); i++)
        {
            // make sure entry isn't blank
            if (keys.get(i).compareTo("") != 0 && values.get(i).compareTo("") != 0)
            {
                // create new entry and add to entry vector
                UrSQLEntry entry = new UrSQLEntry(keys.get(i), values.get(i));
                returnEntity.addEntry(entry);
            }
        }
        returnEntity.setId(id);
        return returnEntity;
    }

    /**
     * Get an entity that matches the given query
     * @param query Keys/values contained in entity
     * @return Entity with given parameters
     */
    public UrSQLEntity getEntity(String query)
    {
        // get ID from query and use above function
        int id = getIdFromQuery(query);
        UrSQLEntity returnEntity = getEntity(id);
        return returnEntity;
    }

    /**
     * Get all entities and correspondoing entries in the database
     * @return List containing all entries
     */
    public Vector<UrSQLEntity> getAllEntities()
    {
        Vector<Integer> allIds = getAllIds();
        int highestId = 0;
        // find highest existing ID
        for (int i = 0; i < allIds.size(); i++)
        {
            if (allIds.get(i) > highestId)
            {
                highestId = allIds.get(i);
            }
        }
        // create new vector to return
        Vector<UrSQLEntity> entities = new Vector<UrSQLEntity>();
        // iterate through entities, skipping 0th
        for (int i = 1; i <= highestId; i++)
        {
            // get entity corresponding to id
            UrSQLEntity entity = getEntity(i);
            // if entity exists, add it to return vector
            if (entity.countEntries() != 0)
            {
                entities.add(entity);
            }
        }
        return entities;
    }

    /**
     * Create a new entry in the entity with the given id
     * @param id ID of entity containing new entry
     * @param query Key/value pair for new entry
     * @return Newly created entry
     */
    private UrSQLEntry createEntry(int id, String query)
    {
        // iterate through file
        for (int i = 0; i < filio.getTotalLines(); i++)
        {
            // read each line
            String line = filio.read(i);
            // check if line is an id line
            if (isIdLine(line))
            {
                // check if id matches
                if (idToInt(line) == id)
                {
                    // append newline followed by key/value pair
                    String writeString = System.getProperty("line.separator") + query;
                    filio.append(writeString, i);
                }
            }
        }
        // return newly created entry
        UrSQLEntry entry = new UrSQLEntry(getKeyFromQuery(query), getValueFromQuery(query));
        UrSQLEntity entity = getEntity(id);
        entry.setEntity(entity);
        return entry;
    }

    /**
     * Create a new entry in the given entity with formatted keys and values
     * @param entity Entity containing new entry
     * @param query Key/value pair for new entry
     * @return Newly created entry
     */
    public UrSQLEntry createEntry(UrSQLEntity entity, String query)
    {
        // get ID from entity then use above method
        return createEntry(entity.getId(), query);
    }

    /**
     * Create a new entry in the given entity with the given key and value
     * @param entity Entity containing new entry
     * @param key Key for new entry
     * @param value Value for new entry
     * @return Newly created entry
     */
    public UrSQLEntry createEntry(UrSQLEntity entity, String key, String value)
    {
        // create query from key and value
        String query = key + KEY_VALUE_SEPARATOR + value;
        // get ID from entity and use above method
        return createEntry(entity.getId(), query);
    }

    /**
     * Create an entry in the given entity
     * @param entity Entity for new entry
     * @param entry New entry
     * @return Newly created entry
     */
    public UrSQLEntry createEntry(UrSQLEntity entity, UrSQLEntry entry)
    {
        // create query from entry's key and value
        String query = entry.getKey() + KEY_VALUE_SEPARATOR + entry.getValue();
        // get id from entity and use above method
        int id = entity.getId();
        return createEntry(id, query);
    }

    /**
     * Create a new entity containing no entries with a unique id
     */
    public UrSQLEntity createEntity()
    {
        // unique ID is highest existing ID plus one
        int highestId = 0;
        Vector<Integer> allIds = getAllIds();
        // find highest existing ID
        for (int i = 0; i < allIds.size(); i++)
        {
            if (allIds.get(i) > highestId)
            {
                highestId = allIds.get(i);
            }
        }
        // increment ID so new ID is unique
        highestId++;
        // write new id to file
        String writeString = "[" + highestId + "]";
        filio.append(writeString);
        // return newly created entity, which has no entries
        UrSQLEntity entity = new UrSQLEntity();
        entity.setId(highestId);
        return entity;
    }

    /**
     * Remove an entry with the given key that matches the given query
     * @param key Key of entry to remove
     * @param query Formatted key/value pairs
     */
    public void removeEntry(String key, String query)
    {
        // get entity matching query
        UrSQLEntity entity = getEntity(query);
        // could also use entity.getEntry(key)
        UrSQLEntry entry = getEntry(key, query);
        removeEntry(entity, entry);
    }

    /**
     * Remove the given entry
     * @param entity Entity where entry is contained
     * @param entry Entry to remove
     */
    public void removeEntry(UrSQLEntity entity, UrSQLEntry entry)
    {
        // create query based on entry keys and values
        String matchLine = entry.getKey() + KEY_VALUE_SEPARATOR + entry.getValue();
        // get ID to entry and create ID line to match
        int id = entity.getId();
        boolean foundId = false;
        String compare = "[" + id + "]";

        // iterate through file
        for (int i = 0; i < filio.getTotalLines(); i++)
        {
            // read each line
            String line = filio.read(i);

            // next ID line has been found
            if (isIdLine(line))
            {
                foundId = false;
            }

            // correct ID line has been found
            if (line.compareTo(compare) == 0)
            {
                foundId = true;
            }
            // remove all lines after ID line up to next ID line
            if (foundId && line.compareTo(compare) != 0)
            {
                if (line.compareTo(matchLine) == 0)
                {
                    filio.delete(i);
                    return;
                }
            }
        }
    }

    /**
     * Remove entity containing given parameters
     * @param query Formatted key/value pairs
     */
    public void removeEntity(String query)
    {
        // get entity from query and remove it
        UrSQLEntity entity = getEntity(query);
        removeEntity(entity);
    }

    /**
     * Remove given entity
     * @param entity Entity to remove
     */
    public void removeEntity(UrSQLEntity entity)
    {
        // get all entries in entity
        Vector<UrSQLEntry> entries = entity.getEntries();
        // iterate through each entry and remove it
        for (int i = 0; i < entries.size(); i++)
        {
            removeEntry(entity, entries.get(i));
        }
    }

    public void writeEntry(UrSQLEntity entity, UrSQLEntry entry)
    {
        // create entry to remove based on original values of entry
        // argument entry does not exist yet in the file, since its values have
        // most likely been changed using entry.setValue, and firstKey and firstValues
        // preserve the values that have been stored in the file
        UrSQLEntry removeEntry = new UrSQLEntry(entry.getFirstKey(), entry.getFirstValue());
        // save key/value pair before deleting
        String key = entry.getKey();
        String value = entry.getValue();
        // remove entry from file
        removeEntry(entity, removeEntry);
        // create new entry and save to file
        UrSQLEntry newEntry = new UrSQLEntry(key, value);
        createEntry(entity, newEntry);
    }

    /**
     * Write the given entry in the given entity to file
     * @param entry Entry to write to file
     */
    public void writeEntry(UrSQLEntry entry)
    {
        // create entry to remove based on original values of entry
        // argument entry does not exist yet in the file, since its values have
        // most likely been changed using entry.setValue, and firstKey and firstValues
        // preserve the values that have been stored in the file
        UrSQLEntry removeEntry = new UrSQLEntry(entry.getFirstKey(), entry.getFirstValue());
        // save key/value pair before deleting
        String key = entry.getKey();
        String value = entry.getValue();
        if (entry.getEntity() == null)
        {
            throw new IllegalArgumentException();
        }
        UrSQLEntity entity = entry.getEntity();
        // remove entry from file
        removeEntry(entity, removeEntry);
        // create new entry and save to file
        UrSQLEntry newEntry = new UrSQLEntry(key, value);
        createEntry(entity, newEntry);
    }

    /**
     * Write each entry in an entity to file
     * @param entity Entity to write to file
     */
    public void writeEntity(UrSQLEntity entity)
    {
        Vector<UrSQLEntry> entries = entity.getEntriesAsVector();
        for (int i = 0; i < entries.size(); i++)
        {
            writeEntry(entries.get(i));
        }
    }

    /**
     * Wipe out the file specified in the constructor to create a new,
     * blank database at that path
     */
    public void createDatabase()
    {
        // wipe out all existing data and create first entity
        filio.write(BLANK_DB_FIRST_LINE);
    }

    /**
     * Check if database contains any entries
     * @return True if database does not contains entries, false if not
     */
    public boolean isEmpty()
    {
        // read database text file
        String dbContents = filio.read();
        return (dbContents.compareTo(BLANK_DB_FIRST_LINE) == 0 || 
                dbContents.compareTo(BLANK_DB_FIRST_LINE + System.getProperty("line.separator")) == 0)
                ? true : false;
    }
}